﻿using Microsoft.Practices.EnterpriseLibrary.Common.Utility;
using Microsoft.Practices.EnterpriseLibrary.Logging;
using HashTag;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
using System.Security.Principal;
using System.Text;
using System.Threading.Tasks;
using System.Web.Security;

namespace Microsoft.Practices.EnterpriseLibrary.Logging
{
    /// <summary>
    /// Collects content that will be injected into an EntLib.LogEntry object
    /// </summary>
    public class LogMessageBuilder
    {
        private LogEntry _entry;
        private NameValueCollection _serverVariables;
        private NameValueCollection _queryString;
        private NameValueCollection _cookies;
        private List<LogException> _exceptions;
        private NameValueCollection _session;
        private NameValueCollection _machine;
        private NameValueCollection _userSettings;

        private DateTime _timestamp = DateTime.Now;
        private Guid _activityId = CoreConfig.ActivityId;
        private string _appDomainName = AppDomain.CurrentDomain.FriendlyName;
        private TraceEventType _severity;

        /// <summary>
        /// Default constructor
        /// </summary>
        public LogMessageBuilder()
            : this(new LogEntry())
        {

        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="entry">Hydrated LogEntry object that will be assigned values that are collected</param>
        public LogMessageBuilder(LogEntry entry)
        {
            _entry = entry;
            
        }

        /// <summary>
        /// Returns an EntLib.LogEntry object suitable for use in EntLib Logging Application Block (LAB).  Applies all collected values in the builder to this object.  
        /// Similar in concept to StringBuilder.ToString()
        /// </summary>
        public LogEntry Entry
        {
            get
            {
                if (_entry == null)
                {
                    _entry = new LogEntry();
                }
                return ApplySettings(_entry);
                
            }
        }

        internal LogMessageBuilder EventType(TraceEventType eventType)
        {
            _severity = eventType;
            return this;
        }


        /// <summary>
        /// Set the priority of the message being build.  (By default, priority is calculated based on EventType) Alias for .Priority
        /// </summary>
        /// <param name="priority"></param>
        /// <returns></returns>
        public LogMessageBuilder Pr(int priority)
        {
            return Priority(priority);
        }

        /// <summary>
        /// Set the priority of the message being build.  (By default, priority is calculated based on EventType)
        /// </summary>
        /// <param name="priority"></param>
        /// <returns></returns>
        public LogMessageBuilder Priority(int priority)
        {
            _entry.Priority = priority;
            return this;
        }

        /// <summary>
        /// Integral identifier of this event.  Used by systems that use id's as primary filtering mechanism.  (By default, eventId is calculated based on EventType) Alias for .EventId
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public LogMessageBuilder Id(int id)
        {
            _entry.EventId = id;
            return this;
        }

        /// <summary>
        /// Retturns a reference to writer that will actaully presist message to log sink.  Used by advanced scenario's.
        /// </summary>
        public LogWriter Writer { get; internal set; }

        /// <summary>
        /// Add full exception details and all inner exceptions into builder.  May call this multiple times if necessary to collect several exceptions. (storage:    entry.ExtendedProperties[Exception[x]]) Alias for .Catch
        /// </summary>
        /// <param name="ex">Hydrated exception to capture</param>
        /// <returns></returns>
        public LogMessageBuilder Ca(Exception ex)
        {
            return Catch(ex);
        }
        /// <summary>
        /// Add full exception details and all inner exceptions into builder.  May call this multiple times if necessary to collect several exceptions. (storage:    entry.ExtendedProperties[Exception[x]]) Alias for .Catch
        /// </summary>
        /// <param name="ex">Hydrated exception to capture</param>
        /// <returns></returns>
        public LogMessageBuilder Ex(Exception ex)
        {
            return Catch(ex);
        }

        /// <summary>
        /// Add full exception details and all inner exceptions into builder.  May call this multiple times if necessary to collect several exceptions. (storage:    entry.ExtendedProperties[Exception[x]])
        /// </summary>
        /// <param name="ex">Hydrated exception to capture</param>
        /// <returns></returns>
        public LogMessageBuilder Catch(Exception ex)
        {
            if (_exceptions == null)
            {
                _exceptions = new List<LogException>();
            }
            
            _exceptions.Add(new LogException(ex));
            return this;
        }

        /// <summary>
        /// Capture the details of the current HTTPContext.  Note: this is done automatically on Critical, Error, Warning levels.  Alias for .HttpContext
        /// </summary>
        /// <returns></returns>
        public LogMessageBuilder Http()
        {
            return HttpContext();
        }

        private static string[] killWords = new string[] { "ALL_HTTP", "ALL_RAW","AUTH_PASSWORD" };
        /// <summary>
        /// Capture the details of the current HTTPContext.  Note: this is done automatically on Critical, Error, Warning levels
        /// </summary>
        /// <returns></returns>
        public LogMessageBuilder HttpContext()
        {
            if (System.Web.HttpContext.Current == null) return this;
            if (System.Web.HttpContext.Current.Request == null) return this;

            var request = System.Web.HttpContext.Current.Request;

            var cookies = System.Web.HttpContext.Current.Request.Cookies;
            if (cookies != null && cookies.Count > 0)
            {
                _cookies = new NameValueCollection();
                for (int x = 0; x < cookies.Count; x++)
                {
                    var cookie = cookies[x];
                    var name = cookie.Name;
                    var cookiePattern = string.Format("D: {0} P: {1} E: {2:yyyy-mm-ddTHH:MM:ss} H: {3} Se: {4} Sh: {5} V: {6}",
                        cookie.Domain,
                        cookie.Path,
                        cookie.Expires,
                        cookie.HttpOnly.ToString(),
                        cookie.Secure.ToString(),
                        cookie.Shareable.ToString(),
                        cookie.Value);
                    _cookies.Add(string.Format("HttpRequest.Cookie[{0}]", name), cookiePattern);
                    if (cookie.HasKeys)
                    {
                        for (int y = 0; y < cookie.Values.Count; y++)
                        {
                            _cookies.Add(string.Format("HttpRequest.Cookie[{0}.{1}]={2}", name, cookie.Values.Keys[x]), cookie.Values[x]);
                        }
                    }
                }
            }

            var queryString = request.QueryString;

            if (queryString != null)
            {
                _queryString = new NameValueCollection();
                for (int x = 0; x < queryString.Count; x++)
                {
                    string key = queryString.AllKeys[x];
                    string value = queryString.Get(x);
                    _serverVariables.Add(string.Format("HttpRequest.QueryString[{0}]", key), value);
                }
            }
            var serverVariables = System.Web.HttpContext.Current.Request.ServerVariables;
            _serverVariables = new NameValueCollection();
            for (int x = 0; x < serverVariables.Count; x++)
            {
                string key = serverVariables.AllKeys[x];
                if (killWords.Any(w => string.Compare(w, key) == 0))
                    continue;
                string value = serverVariables.Get(x);
                _serverVariables.Add(string.Format("HttpRequest.ServerVariable[{0}]", key), value);
            }

            var session = System.Web.HttpContext.Current.Session;
            _session = new NameValueCollection();
            _session.Add("HttpRequest.Session.CodePage", session.CodePage.ToString());
            _session.Add("HttpRequest.Session.CookieMode", session.CookieMode.ToString());
            _session.Add("HttpRequest.Session.IsCookieless", session.IsCookieless.ToString());
            _session.Add("HttpRequest.Session.IsNewSession", session.IsNewSession.ToString());
            _session.Add("HttpRequest.Session.IsReadOnly", session.IsReadOnly.ToString());
            _session.Add("HttpRequest.Session.LCID", session.LCID.ToString());
            _session.Add("HttpRequest.Session.Mode", session.Mode.ToString());
            _session.Add("HttpRequest.Session.Timeout", session.Timeout.ToString());
            if (_session.HasKeys() == true)
            {
                for (int x = 0; x < session.Keys.Count; x++)
                {
                    string key = session.Keys[x];
                    string value = string.Format("{0}", session[x]);
                    string valueType = (value == null) ? "(null)" : value.GetType().Name;
                    _serverVariables.Add(string.Format("HttpRequest.Session[{0} ({1}]", key, valueType), value);
                }
            }

            return this;
        }

        /// <summary>
        /// Capture the details of the current MachineContext.  Note: this is done automatically on Critical, Error, Warning levels Alias for .MachineContext
        /// </summary>
        /// <returns></returns>
        public LogMessageBuilder Os()
        {
            return MachineContext();
        }
        /// <summary>
        /// Capture the details of the current MachineContext.  Note: this is done automatically on Critical, Error, Warning levels
        /// </summary>
        /// <returns></returns>
        public LogMessageBuilder MachineContext()
        {
            _machine = new NameValueCollection();
            _machine.Add("Machine.CommandLine", Environment.CommandLine);
            _machine.Add("Machine.CurrentDirectory", Environment.CurrentDirectory);
            _machine.Add("Machine.ManagedThreadId", Environment.CurrentManagedThreadId.ToString());
            _machine.Add("Machine.HasShutdownStarted", Environment.HasShutdownStarted.ToString());
            _machine.Add("Machine.Is64BitOS", Environment.Is64BitOperatingSystem.ToString());
            _machine.Add("Machine.Is64BitProcess", Environment.Is64BitProcess.ToString());
            _machine.Add("Machine.MachineName", Environment.MachineName);
            _machine.Add("Machine.OSVersion", Environment.OSVersion.ToString());
            _machine.Add("Machine.ProcessorCount", Environment.ProcessorCount.ToString());
            _machine.Add("Machine.StackTrace", Environment.StackTrace);
            _machine.Add("Machine.TickCount", Environment.TickCount.ToString());
            _machine.Add("Machine.UserDomainName", Environment.UserDomainName);
            _machine.Add("Machine.UserName", Environment.UserName);
            _machine.Add("Machine.Version", Environment.Version.ToString());
            _machine.Add("Machine.WorkingSet", Environment.WorkingSet.ToString());

            return this;
        }

        /// <summary>
        /// Capture the details of the current user (both Thread and HttpContext.User.  Note: this is done automatically on Critical, Error, Warning levels. Alias for .UserContext
        /// </summary>
        /// <returns></returns>
        public LogMessageBuilder User()
        {
            return UserContext();
        }

        /// <summary>
        /// Capture the details of the current user (both Thread and HttpContext.User.  Note: this is done automatically on Critical, Error, Warning levels
        /// </summary>
        /// <returns></returns>
        public LogMessageBuilder UserContext()
        {
            _userSettings = new NameValueCollection();
            if (System.Web.HttpContext.Current != null && System.Web.HttpContext.Current.User != null)
            {
                var user = System.Web.HttpContext.Current.User;
                _userSettings.Add("WebUser.PrincipalType", user.GetType().Name);
                if (user.Identity != null)
                {

                    _userSettings.Add("WebUser.IdentityType", user.Identity.GetType().Name);
                    _userSettings.Add("Webuser.IdentityName", user.Identity.Name);
                    _userSettings.Add("WebUser.IdentityAuthenticationType", user.Identity.AuthenticationType);
                    _userSettings.Add("WebUser.IdentityIsAuthenticated", user.Identity.IsAuthenticated.ToString());
                    var rp = user as RolePrincipal;
                    if (rp != null)
                    {
                        var roleList = rp.GetRoles();
                        for (int x = 0; x < roleList.Length; x++)
                        {
                            _userSettings.Add(string.Format("WebUser.Role[{0}]", x), roleList[x]);
                        }
                    }
                }
            }

            if (System.Threading.Thread.CurrentPrincipal != null)
            {
                var user = System.Threading.Thread.CurrentPrincipal;
                _userSettings.Add("Thread.PrincipalType", user.GetType().Name);
                if (user.Identity != null)
                {
                    _userSettings.Add("Thread.IdentityType", user.Identity.GetType().Name);
                    _userSettings.Add("Thread.IdentityName", user.Identity.Name);
                    _userSettings.Add("Thread.IdentityAuthenticationType", user.Identity.AuthenticationType);
                    _userSettings.Add("Thread.IdentityIsAuthenticated", user.Identity.IsAuthenticated.ToString());
                }
            }

            return this;
        }

        /// <summary>
        /// Grab the current stack trace. Alias for .Stack
        /// </summary>
        /// <returns></returns>
        public LogMessageBuilder St()
        {
            return Stack();
        }

        private string _stack;
        /// <summary>
        /// Grab the current stack trace 
        /// </summary>
        /// <returns></returns>
        public LogMessageBuilder Stack()
        {
            _stack = Environment.StackTrace;
            return this;
        }

        /// <summary>
        /// Add a category (called 'TraceSource' in EntLib.LAB configuration) (storage: le.Categories) Alias for .Category
        /// </summary>
        /// <param name="categoryName"></param>
        /// <returns></returns>
        public LogMessageBuilder Ca(string categoryName)
        {
            return Category(categoryName);
        }

        /// <summary>
        /// Add a category (called 'TraceSource' in EntLib.LAB configuration) (storage: le.Categories)
        /// </summary>
        /// <param name="categoryName"></param>
        /// <returns></returns>
        public LogMessageBuilder Category(string categoryName)
        {
            _entry.Categories.Add(categoryName);
            return this;
        }

        /// <summary>
        /// Set's the priority for this message.  Note: EntLib higher priorities are lower numbers. Alias for Priority
        /// </summary>
        /// <param name="priority"></param>
        /// <returns></returns>
        public LogMessageBuilder Pr(int? priority = null)
        {
            return Priority(priority);
        }

        /// <summary>
        /// Set's the priority for this message.  Note: EntLib higher priorities are lower numbers
        /// </summary>
        /// <param name="priority"></param>
        /// <returns></returns>
        public LogMessageBuilder Priority(int? priority = null)
        {
            if (priority.HasValue == true)
            {
                _entry.Priority = priority.Value;
            }
            else
            {
                _entry.Priority = 0;
            }
            return this;
        }

        /// <summary>
        /// Add a key-value pair to the message's ExtendedProperties collection. Alias for .Collect
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public LogMessageBuilder Co(string key, object value)
        {
            return Collect(key, value);
        }
        /// <summary>
        /// Add a key-value pair to the message's ExtendedProperties collection.  Alias for .Collect
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public LogMessageBuilder Prop(string key, object value)
        {
            return Collect(key, value);
        }

        /// <summary>
        /// Add a key-value pair to the message's ExtendedProperties collection. Alias for .Collect
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public LogMessageBuilder Co(string key, string text, params object[] args)
        {
            return Collect(key, text, args);
        }
        /// <summary>
        /// Add a key-value pair to the message's ExtendedProperties collection.  Alias for .Collect
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public LogMessageBuilder Prop(string key, string text, params object[] args)
        {
            return Collect(key, text, args);
        }

        /// <summary>
        /// Add a key-value pair to the message's ExtendedProperties collection
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public LogMessageBuilder Collect(string key, object value)
        {
            _entry.ExtendedProperties[key] = value;
            return this;
        }

        /// <summary>
        /// Add a key-value (string-string) pair to the message's ExtendedProperties collection
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public LogMessageBuilder Collect(string key, string text, params object[] args)
        {
            _entry.ExtendedProperties[key] = string.Format(text, args);
            return this;
        }

        /// <summary>
        /// Persist message with accumulated values to the log sink. Alias for .Write
        /// </summary>
        public void W()
        {
            Write();
        }

        /// <summary>
        /// Persist message with accumulated values to the log sink
        /// </summary>
        public void Write()
        {
            var logEvent = Entry;

            if (string.IsNullOrEmpty(logEvent.Message) == true)
            {
                if (_exceptions != null && _exceptions.Count > 0)
                {
                    logEvent.Message = _exceptions[0].Message;
                }
            }

            if (string.IsNullOrEmpty(logEvent.Title) == true)
            {
                if (string.IsNullOrEmpty(logEvent.Message) == false)
                {
                    logEvent.Title = logEvent.Message.Left(70, "...");
                }
            }

            if (Writer.ShouldLog(logEvent) == false)
            {
                return;
            }

            Writer.Write(logEvent);
        }
        /// <summary>
        /// Write log entry to EntLib log sink with a custom message. Alias for .Write
        /// </summary>
        /// <param name="message">Message with any standard string.format() qualifiers</param>
        /// <param name="args">Arguments to supply to message</param>
        public void W(string message, params object[] args)
        {
            Write(message, args);
        }

        /// <summary>
        /// Write log entry to EntLib log sink with a custom message
        /// </summary>
        /// <param name="message">Message with any standard string.format() qualifiers</param>
        /// <param name="args">Arguments to supply to message</param>
        public void Write(string message, params object[] args)
        {

            if (message != null)
            {
                Entry.Message = string.Format(message, args);
            }

            Write();
        }

        /// <summary>
        /// Apply collected settings to <paramref name="entry"/>.  Generally not used by calling application.  Use either Entry property or Write() method instead
        /// </summary>
        /// <param name="entry">Hydrated EntLib.LogEntry or null to return a new LogEnty</param>
        /// <returns>New entry or existing entry will values set by contents of builder</returns>
        public LogEntry ApplySettings(LogEntry entry = null)
        {

            if (entry == null)
            {
                entry = new LogEntry();
            }
            else
            {
                entry = _entry;
            }

            entry.Severity = _severity;

            entry.TimeStamp = DateTime.Now;

            if (_exceptions != null && _exceptions.Count > 0)
            {
                for (int x = 0; x < _exceptions.Count; x++)
                {
                    entry.AddErrorMessage(_exceptions[x].Message);
                    entry.ExtendedProperties[string.Format("Exception[{0}]", x)] = _exceptions[x];
                }
            }

            if (entry.Priority == -1)
            {
                entry.Priority = (int)_severity;
            }

            if (entry.EventId == 0)
            {
                if (entry.Priority != (int)entry.Severity) //explictly set priority so adjust magnitude to host both severity and priority
                {
                    entry.EventId = (((int)entry.Severity) * 1000) + (int)entry.Priority;
                }
                else // use severity as event id
                {
                    entry.EventId = (int)entry.Severity;
                }
            }

            if (_machine != null)
            {
                foreach (var setting in _machine.AllKeys)
                {
                    entry.ExtendedProperties[setting] = _machine[setting];
                }
            }
            if (_userSettings != null)
            {
                foreach (var setting in _userSettings.AllKeys)
                {
                    entry.ExtendedProperties[setting] = _userSettings[setting];
                }
            }
            if (_serverVariables != null)
            {
                foreach (var setting in this._serverVariables.AllKeys)
                {
                    entry.ExtendedProperties[setting] = _serverVariables[setting];
                }
            }

            if (_session != null)
            {
                foreach (var setting in this._session.AllKeys)
                {
                    entry.ExtendedProperties[setting] = _session[setting];
                }
            }
            if (_cookies != null)
            {
                foreach (var setting in this._cookies.AllKeys)
                {
                    entry.ExtendedProperties[setting] = _cookies[setting];
                }
            }
            return entry;
        }
    }
}
